Simply:
Let's start with something basic:
In [1]:
typeof(1), typeof(1.), typeof(1.f0), typeof('a'), typeof("foo")
Out[1]:
In [2]:
A = rand(5, 5)
Out[2]:
In [3]:
typeof(A), eltype(A)
Out[3]:
Clearly, Julia puts types front and center. Contrast this to Python, where it's possible, but not trivial or syntactically nice, to get the name of an object's superclass (obj.__class__.__bases__) or test for whether an object is a subclass (issubclass).
This is because Python is built on duck typing, and the focus is on behaviors that just work. This is a key philosophical point: you can be a very good Python programmer and worry very little about inheritance and types. Just define classes, add methods, and move on.
In Julia, everything is organized around types:
In [4]:
T = typeof(1.) # a type is a variable
Out[4]:
In [5]:
typeof(T)
Out[5]:
In [6]:
typeof(DataType) # DataType is its own type
Out[6]:
In [7]:
super(T) # --> supertype in v0.5
Out[7]:
In [8]:
super(AbstractFloat)
Out[8]:
In [9]:
super(Real)
Out[9]:
In [10]:
super(Number)
Out[10]:
In [11]:
super(Any) # Any is its own supertype
Out[11]:
In [12]:
super(DataType)
Out[12]:
In [13]:
super(super(DataType)) # Any really is the top of the hierarchy
Out[13]:
In [14]:
subtypes(Number)
Out[14]:
In [15]:
subtypes(Real)
Out[15]:
We can use the <: operator to test for subtyping, too:
In [16]:
Float64 <: Real, Int64 <: AbstractFloat
Out[16]:
And we can use the isa function for testing instances:
In [17]:
isa(1, Float64), isa(1., Float64), isa(1, Number)
Out[17]:
When it makes sense, we can use convert or simply the type name to convert:
In [18]:
convert(Int64, 1.), convert(Float32, 3.75), convert(Rational{Int64}, 3.75)
Out[18]:
In Julia, only leaf nodes in the type tree (types with no subtypes) are concrete and can be instantiated. That is, variables can only have concrete types, and no concrete type can have subtypes. This seems limiting, and is, but drastically speeds up type inference and performance and pushes us toward composition over inheritance.
In [19]:
isleaftype(Int64), isleaftype(AbstractFloat)
Out[19]:
It often allows for more generic code if types can take parameters. For instance:
In [20]:
A = rand(5, 5)
isa(A, Array{Float64}), isa(A, Array)
Out[20]:
In [21]:
B = reshape(1:25, 5, 5)
isa(B, Array{Int64}), isa(B, Array)
Out[21]:
That is, we'd like to write code that works for A and B. In defining functions, we can do this in a couple of ways:
In [22]:
function lastelem(A::Array) # new syntax: restrict this definition to A's that are Arrays
return A[end]
end
Out[22]:
In [23]:
lastelem(A), lastelem(B)
Out[23]:
We can also restrict the element types we will allow:
In [24]:
function diagsum{T<:Number}(A::Array{T})
return sum(diag(A))
end
Out[24]:
In [25]:
diagsum(A)
Out[25]:
In [26]:
C = reshape([c for c in "abcdefghijklmnopqrstuvwxy"], 5, 5)
Out[26]:
In [27]:
diagsum(C)
The best example of this is Array{T, N}, where the first parameter is the element type and the second is the number of dimensions. Later, we'll look at how to add parameters when we create our own types.
Technical The following facts are very important:
In [28]:
typeof(A)
Out[28]:
In [29]:
eltype(A) <: Real
Out[29]:
In [30]:
typeof(A) <: Array{Real}
Out[30]:
It is natural to think this should hold in Julia, but it doesn't. cf. here for why.
There is, however, an important exception:
In [31]:
tt = (1.5, 2)
typeof(tt)
Out[31]:
In [32]:
typeof(tt) <: Tuple{Real, Real}
Out[32]:
Short answer: we must have this for function argument checking to work correctly, since collections to arguments of functions are checked as tuples. That is tuples must be covariant for f(x::Real, y::Real) to accept an integer and a floating point number.
So why all this fuss about types? Types and the type system are what allow multiple dispatch to work, and multiple dispatch and types are the key organizing principle of Julia code.